using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.IO;

namespace OpenWaves.Caching
{
    public class FileSystemCache<T> : ICache<T>, IDisposable where T : class
    {
        private readonly string cacheDirectory;
        private readonly Action<T, Stream> serialize;
        private readonly Func<Stream, T> deserialize;
        private readonly TextWriter fileNameLookupFile;

        private readonly ConcurrentDictionary<string, FileDescriptor> fileLookup = new ConcurrentDictionary<string, FileDescriptor>();

        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope")]
        public FileSystemCache(string cacheDirectory, Action<T, Stream> serialize, Func<Stream, T> deserialize)
        {
            this.cacheDirectory = cacheDirectory;
            this.serialize = serialize;
            this.deserialize = deserialize;

            if (Directory.Exists(cacheDirectory) == false)
                Directory.CreateDirectory(cacheDirectory);

            var keysFilePath = Path.Combine(cacheDirectory, "keys");

            if (File.Exists(keysFilePath))
            {
                using (var lookupFile = File.OpenText(keysFilePath))
                {
                    string line;
                    while ((line = lookupFile.ReadLine()) != null)
                    {
                        var command = line.Substring(0, "ADD".Length);
                        var fileName = line.Substring("ADD".Length+1, Guid.Empty.ToString().Length);
                        var key = line.Substring("ADD".Length+1 +fileName.Length+1);

                        switch (command)
                        {
                            case "ADD":
                                this.fileLookup.TryAdd(key, new FileDescriptor(fileName));
                                break;
                            case "DEL":
                                FileDescriptor file;
                                this.fileLookup.TryRemove(key, out file);
                                break;
                            default:
                                throw new InvalidOperationException(ExceptionMessage.Format("Unknown command: {0}.", command));
                        }
                    }
                }
            }

            this.fileNameLookupFile = new StreamWriter(File.Open(keysFilePath, FileMode.Append));
        }

        public void InsertValue(string key, T value, DateTime absoluteExpiration)
        {
            var tempFileName = Path.GetTempFileName();
            using (var stream = File.OpenWrite(tempFileName))
            {
                var writer = new BinaryWriter(stream);
                writer.Write(absoluteExpiration.ToBinary());
                this.serialize(value, stream);
            }

            var newFile = new FileDescriptor(Guid.NewGuid().ToString());
            var file = this.fileLookup.GetOrAdd(key, newFile);
            if (file == newFile)
            {
                this.fileNameLookupFile.WriteLine("ADD|" + file + "|" + key);
                this.fileNameLookupFile.Flush();
            }

            lock (file)
            {
                var filePath = Path.Combine(this.cacheDirectory, file.FileName);
                var backupFilePath = filePath + ".tmp";

                if (File.Exists(filePath))
                    File.Move(filePath, backupFilePath);

                File.Move(tempFileName, filePath);      
          
                if (File.Exists(backupFilePath))
                    File.Delete(backupFilePath);
            }
        }

        public T GetValue(string key)
        {
            FileDescriptor file;
            if (this.fileLookup.TryGetValue(key, out file) == false)
                return null;

            lock(file)
            {
                var filePath = Path.Combine(this.cacheDirectory, file.FileName);
                if (File.Exists(filePath) == false)
                    filePath = filePath + ".tmp";
                if (File.Exists(filePath) == false)
                {
                    this.EvictValue(key);
                    return null;
                }

                using (var stream = File.OpenRead(filePath))
                {
                    var expirationTime = DateTime.FromBinary(new BinaryReader(stream).ReadInt64());
                    if (expirationTime < Time.Now)
                    {
                        stream.Close();
                        this.EvictValue(key);
                        return null;                        
                    }

                    return this.deserialize(stream);
                }
            }
        }

        public void EvictValue(string key)
        {
            FileDescriptor file;
            if (this.fileLookup.TryRemove(key, out file) == false)
                return;

            lock (file)
            {
                var filePath = Path.Combine(this.cacheDirectory, file.FileName);
                if (File.Exists(filePath))
                    File.Delete(filePath);
                if (File.Exists(filePath + ".tmp"))
                    File.Delete(filePath + ".tmp");

                this.fileNameLookupFile.WriteLine("DEL|" + file + "|" + key);
                this.fileNameLookupFile.Flush();
            }
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            this.fileNameLookupFile.Dispose();
        }

        private class FileDescriptor
        {
            public string FileName { get; private set; }

            public FileDescriptor(string fileName)
            {
                this.FileName = fileName;
            }

            public override string ToString()
            {
                return this.FileName;
            }
        }
    }
}